package dse.modules;

import java.util.Collections;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Vector;

import testing.TestHelper;
import dse.Cluster;
import dse.DataObject;
import dse.DataObjectAccessRanking;
import dse.DseException;
import dse.Network;
import dse.Node;
import dse.NodeState;
import dse.ExperimentConfiguration;
import dse.Experiment;
import dse.modules.eln.EarthLikeNetwork;

public class ThresholdCacheDecisionMaker implements CacheDecisionMaker {
	private int periodsToAnalyze;
	private double threshold;
	private LinkedList<Map<Cluster, Map<DataObject,Integer>>> requestMemory;
	
	public void initalize (
			ExperimentConfiguration systemSettings, 
			Map<String, String> moduleSettings
			)
	throws DseException
	{
		requestMemory = new LinkedList<Map<Cluster,Map<DataObject,Integer>>>();
		
		ModuleUtil.SettingsReader sr = ModuleUtil.getSettingsReader(
				this.getClass(), moduleSettings
				);
		periodsToAnalyze =  sr.getInteger("periods-to-analyze");
		threshold = sr.getDouble("threshold") / 100;
	}
	
	public Map<Cluster, List<DataObject>> makeCachingDecisions (
			Network network, 
			Map<Node, DataObject> generatedRequests
			)
	{
		Hashtable<Cluster, List<DataObject>> ht = 
			new Hashtable<Cluster, List<DataObject>>();
		
		
		Map<Cluster, Map<DataObject,Integer>> newClusterRequests = 
			groupRequestsByCluster(network, generatedRequests);
		
		requestMemory.addFirst(newClusterRequests);
		if (requestMemory.size() > periodsToAnalyze)
			requestMemory.removeLast();
		
		for (Cluster c : newClusterRequests.keySet()) {
			// For every data object a cluster has requested this time, see
			// if it should be cached...
			Map<DataObject,Integer> clusterRequests = newClusterRequests.get(c);
			for (DataObject d : clusterRequests.keySet()) {
				if (c.isCaching(d, Experiment.currentPeriod) || c.isCachingUnactive(d))
					// Then dont even worry about it!
					continue;
				
				int count = clusterRequests.get(d);
				
				// Make sure this count is above the threshold by itself.  If 
				// it is not, then we would only cache this data object if the 
				// previous request counts for the object exceeded the threshold
				// but if that was the case, then the data object should have 
				// probably been cached then!  Its not a sure thing of course,
				// but I think this restriction cuts down on a lot of expensive
				// operations.
				if (count < c.size()*threshold/periodsToAnalyze)
					continue;
				
				// Calcuate the count for the previous periods as well before 
				// making the final decision.
				count = 0;
				for (Map<Cluster, Map<DataObject,Integer>> creqs : 
					requestMemory)
				{
					Map<DataObject,Integer> doreqsForCluster = creqs.get(c);
					if (doreqsForCluster != null 
							&& doreqsForCluster.get(d) != null)
						count += doreqsForCluster.get(d);
				}
				
				// If we pass the threshold...
				if (count > c.size()*threshold) {
					// Then we must cache the data object!
					List<DataObject> list = null;
					if (ht.containsKey(c))
						list = ht.get(c);
					else {
						list = new LinkedList<DataObject>();
						ht.put(c, list);
					}
					
					list.add(d);
				}
			}
		}
		
		
		return ht;
	}

	private Map<Cluster, Map<DataObject,Integer>> groupRequestsByCluster (
			Network network, 
			Map<Node, DataObject> requests
			)
	{
		Map<Cluster, Map<DataObject,Integer>> rm = 
			new Hashtable<Cluster, Map<DataObject,Integer>>();
		
		for (Node n : requests.keySet()) {
			DataObject dobj = requests.get(n);
			
			Cluster c = network.lookupCluster(n.clusterId());
			Map<DataObject,Integer> m = rm.get(c);
			if (m == null) {
				m = new Hashtable<DataObject, Integer>();
				rm.put(c,m);
			}
			int cv = 0;
			if (m.containsKey(dobj))
				cv = m.get(dobj);
			m.put(dobj, cv+1);
		}
		
		return rm;
	}
	
	
	
	

	/**
	 * For testing :)
	 * 
	 * @param args
	 * @throws Exception
	 */
	public static void main (String [] args) 
	throws Exception
	{
		if (args.length != 7 && args.length != 8) {
			System.out.println(
					"Usage: java -cp ... " +
					"dse.modules.ThresholdCacheDecisionMaker " +
					"<nodes> <max-cluster-size> <num-data-objs> " +
					"<request-frequency> <periods-to-analyze> <threshold> " +
					"<rounds> (seed)"
					);
			System.exit(-1);
		}
		
		
		ExperimentConfiguration ss = TestHelper.createSystemSettings(args, 7);
		
		EarthLikeNetwork network = TestHelper.createELNetwork(
				ss, args, 0, -1, -1
				);
		
		// We could use the GeneralNodeEventScheduler, but this is easier ;)
		for (Node n : network.nodes())
			n.setState(NodeState.Up);
		
		TestHelper.createClusters(ss, args, 1, network);
		
		Vector<DataObject> dataObjects = TestHelper.createDataObjects(
				ss, args, 2, -1, -1, -1, -1
				);
		
		Map<Cluster,DataObjectAccessRanking> rankings = 
			TestHelper.generateDataObjectAccessRankings(
				ss, args, -1, network, dataObjects
				);
		
		GeneralRequestEventScheduler reqSched = 
			TestHelper.createScheduler(ss, args, 3, 2);
		
		ThresholdCacheDecisionMaker tcdm =
			TestHelper.createCacheDecisionMaker(ss, args, 4, 5);
		
		BloomFilterGenerator bfg = TestHelper.createBloomFilterGenerator(ss);
		
		for (Cluster c : network.clusters()) 
			c.setBloomFilter(bfg.generateBloomFilterForCachedDataObjects(c));
		
		int rounds = Integer.parseInt(args[6]);
		
		Map<Cluster, Map<DataObject,Integer>> requestMemory = 
			new Hashtable<Cluster, Map<DataObject,Integer>>();
		
		for (int a = 0; a < rounds; a++) {
			System.out.println("Round #:" + (a+1));
			
			Map<Node, DataObject> requests = 
				reqSched.getRequests(network, rankings);
			
			// Remeber requests!
			for (Node n : requests.keySet()) {
				DataObject dobj = requests.get(n);
				
				Cluster c = network.lookupCluster(n.clusterId());
				Map<DataObject,Integer> m = requestMemory.get(c);
				if (m == null) {
					m = new Hashtable<DataObject, Integer>();
					requestMemory.put(c, m);
				}
				
				int cv = 0;
				if (m.containsKey(dobj))
					cv = m.get(dobj);
				m.put(dobj, cv+1);
			}
			
			// Create caching decisions!
			Map<Cluster, List<DataObject>> cdecs = tcdm.makeCachingDecisions(
					network,
					requests
					);
			
			for (Cluster c : cdecs.keySet()) {
				List<DataObject> dobjsToCache = cdecs.get(c);
				System.out.println(
						"Cluster " + c.id() + ": " + dobjsToCache.size() + 
						" objects to cache."
						);
				
				// Lets cache them!
				for (DataObject d : dobjsToCache)
					c.addCachedDataObject(d, 1, true);
			}
			
			System.out.println();
		}
		

		System.out.println();
		System.out.println();
		System.out.println();
		
		SortedMap<DataObject,String> cachedDOs = 
			new TreeMap<DataObject, String>();
		
		for (Cluster c : network.clusters()) {
			System.out.println("Cluster " + c.id());
			System.out.println("DataObjects w/ Requests > 10");
			Map<DataObject,Integer> rm = requestMemory.get(c);
			Vector<String> output = null;
			if (rm != null) {
				output = new Vector<String>(rm.keySet().size());			
				for (DataObject dobj : rm.keySet())
					if (rm.get(dobj) > 10)
						output.add(
								"\tDataObject " + dobj.id() + 
								" requested:\t" + rm.get(dobj) + " times."
								);
				Collections.sort(output);
				for (String s : output)
					System.out.println(s);
			}
			
			output = new Vector<String>();
			for (DataObject dobj : c.cachedDataObjects()) {
				output.add(dobj.id() + " ");
				if (cachedDOs.get(dobj) == null)
					cachedDOs.put(dobj, ""+c.id());
				else
					cachedDOs.put(dobj, cachedDOs.get(dobj)+", "+c.id());
			}
			Collections.sort(output);
			System.out.println("Cached Data Objects: ");
			for (String s : output)
				System.out.println("\tDataObject "+s);
			System.out.println();
			System.out.println(); 
		}
		
		System.out.println("All Cached Data Objects");
		for (DataObject d : cachedDOs.keySet()) {
			System.out.print("Data Object " + d.id() + " cached in:\t[");
			System.out.print(cachedDOs.get(d));
			System.out.print("]");
			System.out.println();
		}
		System.out.println();
		
		System.out.println("Done.");
	}
}
